# amplitude.wsgi -- wsgi application and support

import os.path
import sys
import types, pkgutil

from amplitude.config import SiteConfig
from amplitude import sessions
from amplitude.http import Request, Response
from amplitude.dict import Dict
from amplitude.string import camelify, identifier

from mako.lookup import Template, TemplateLookup
import mako.exceptions

class Application(Dict):
    
    def __init__(self, config_filepath, **args):
        """initialize the application."""
        Dict.__init__(self, **args)
        self.load_config(config_filepath)
        if self.DEBUG == True: print "loaded amplitude.wsgi.Application, config:", config_filepath
        if self.config.Site.lang is None: self.config.Site.lang = 'english'
        
    def load_config(self, config_filepath):
        """load the application configuration"""
        print "\nApplication.load_config(%s)" % config_filepath
        self.config_filepath = config_filepath
        self.config = SiteConfig(config_filepath)
        
        # site paths, with sensible defaults.
        self.site_root = self.config.Site.site_root or os.path.dirname(config_filepath)
        self.doc_root = os.path.abspath(os.path.join(self.site_root, self.config.Site.doc_root or 'www'))
        self.site_uri = self.config.Site.uri or (self.config.Site.host + (self.config.Site.path or ''))
        self.module_name = self.config.Site.module_name or os.path.basename(os.path.dirname(os.path.abspath(config_filepath)))
        self.DEBUG = eval(self.config.Site.debug)

        if self.DEBUG==True:
            print "  site_root =", self.site_root
            print "  doc_root =", self.doc_root
            print "  site_uri =", self.site_uri
            print "  module_name=", self.module_name
            print "  config =", self.config
        
        # set up template_dirs for lookup: (1) this site's; (2) a master site's, if any; (3) amplitude templates
        self.template_dirs = [os.path.join(self.site_root, self.config.Site.template_dir or 'templates')]
        if self.config.Site.master_site is not None:
            self.template_dirs += [os.path.join(self.config.Site.master_site, self.config.Site.template_dir or 'templates')]
        self.template_dirs += [os.path.join(os.path.dirname(__file__), 'templates')]
        self.template_cache = os.path.join(self.site_root, self.config.Site.template_cache or 'cache/templates')

        self.template_lookup = TemplateLookup(directories = self.template_dirs,
                                         module_directory = self.template_cache,
                                         input_encoding = 'utf-8',
                                         output_encoding = 'utf-8',
                                         filesystem_checks = self.config.Site.status.lower().strip() == 'development')
        
        # set up session storage
        if self.config.Session.storage_class is None: self.config.Session.storage_class = 'SessionStorage'
        if self.config.Session.storage_class == 'SessionStorage':
            self.session_storage = sessions.SessionStorage()
        elif self.config.Session.storage_class == 'DatabaseStorage':
            self.session_storage = sessions.DatabaseStorage(self.db or self.config.connect_to_db())
        elif self.config.Session.storage_class == 'MemcacheStorage':
            self.session_storage = sessions.MemcacheStorage(self.config.Session.memcache_server)
        elif self.config.Session.storage_class == 'CookieStorage':
            self.session_storage = sessions.CookieStorage(self.config.Session.cookie_key)
        elif self.config.Session.storage_class == 'FileStorage':
            self.session_storage = sessions.FileStorage(self.config.Session.directory or 'cache/sessions')

        # set up routes, controllers, models, emails 
        # -- reload for when load_config is called after initial server boot
        self.routes = reload(__import__(self.module_name + '.routes').routes)
        
        # Python 2.6 compatibility: not using dict comprehensions
        self.controllers = Dict()
        for c in get_classes(__import__(self.module_name + '.controllers').controllers):
            self.controllers[identifier(c.__name__)] = c

        self.models = Dict()
        for c in get_classes(__import__(self.module_name + '.models').models):
            self.models[c.__name__] = c

        self.emailers = Dict()
        for c in get_classes(__import__(self.module_name + '.emailers').emailers):
            self.emailers[c.__name__] = c

        # if self.DEBUG==True:
            # print " site_root:", self.site_root
            # print " doc_root:", self.doc_root
            # print " site_uri:", self.site_uri
            # print " site lang:", self.config.Site.lang
            # print ' controllers:', self.controllers.keys()
            # print ' models:', self.models.keys()
            # print ' emailers:', self.emailers.keys()
        
        
    def __call__(self, env, start_response):
        """call the application, each request. env and start_response are the required WSGI arguments."""
        
        # NOTE: Apart from reloading config, don't modify self in __call__ -- keep it thread-safe
        # Use request and response to hold values that are specific to this __call__ cycle.
        
        try:
            # reload configuration if status == 'development'
            if self.config.Site.status == 'development':
                self.load_config(self.config_filepath)
            
            req = Request(env, self.config)
            req.session_storage = self.session_storage  # needed for req.init_session()
            response = Response()
            
            if self.DEBUG == True: print "\nreq.uri =", unicode(req.uri).encode('utf-8')
            
            # Route the request:
            # If the file exists in doc_root, send it.
            filepath = self.doc_root + req.path
            if os.path.exists(filepath.encode('utf-8')) and not os.path.isdir(filepath.encode('utf-8')):
                if self.DEBUG==True: print "  FILE:", filepath.replace('\\', '/')
                f = open(filepath, 'rb')
                response.body = [f.read() or '']
                f.close()
            else:                                                   # Try routing to handle the request
                response.set_header('Content-Type', 'text/html')    # default Content-Type: text/html
                req.route = self.routes.map.match(req.path)         # does req.path match a route?
                controller = None
                if req.route is not None: 
                    if self.DEBUG==True: print '  route =', req.route
                    # first option is routing to a controller
                    if ('controller' in req.route 
                    and req.route.controller is not None 
                    and req.route.controller in self.controllers):
                        controller = self.controllers[req.route.controller]
        
                        # controller.actions can either be defined in the class, or generated here
                        if controller.actions in [None, []]: 
                            # all method names / attributes that don't begin with _
                            controller.actions = [a for a in controller.__dict__.keys() if a[0] != '_']

                        if 'action' in req.route and controller.actions is not None and req.route.action in controller.actions:
                            if self.DEBUG==True: print "  CONTROLLER:", controller.__name__, req.route.action
                            try:
                                c = controller(req, response, **self)
                                response.body = [c(req) or '']
                            except:
                                exc = mako.exceptions.html_error_template().render()
                                if self.config.Site.status == 'development':
                                    response.body = [exc]
                                else:
                                    self.send_error_mail(req, exc)
                                    template = self.template_lookup.get_template('/errors/servererror.mako')
                                    response.body = [template.render(req=req, response=response, c=self) or '']
                        else:
                            response.not_found()
                            template = self.template_lookup.get_template('/404.mako')
                            response.body = [template.render(req=req, response=response, c=self) or '']
                    else:
                        response.not_found()
                        template = self.template_lookup.get_template('/404.mako')
                        response.body = [template.render(req=req, response=response, c=self) or '']

                else:                                               # No route, try rendering a template directly.
                    if req.path == '': req.path = '/'
                    if req.path[-1] == '/': req.path += 'index'
                    if self.template_lookup.has_template(req.path+'.mako'):
                        template = self.template_lookup.get_template(req.path+'.mako')
                    elif self.template_lookup.has_template(req.path+'/index.mako'):
                        template = self.template_lookup.get_template(req.path+'/index.mako')
                    else:
                        response.not_found()
                        template = self.template_lookup.get_template('/404.mako')
                    if self.DEBUG==True: print "  TEMPLATE:", template.filename.replace('\\', '/')
                    response.body = [template.render(req=req, response=response, c=self) or '']

        except:
            exc = mako.exceptions.html_error_template().render()
            if self.config.value('Site', 'status') == 'development':    # exc to screen in development
                raise
                response.body = [exc]
            else:                                                       # exc email to admin in production
                self.send_error_mail(req, exc)
                template = self.template_lookup.get_template('/errors/servererror.mako')
                response.body = [template.render(req=req, response=response, c=self) or '']

        # save the session, if there is one, before sending response.
        if req.session is not None: 
            req.session.save()
            if self.DEBUG==True: print "SESSION SAVED:", req.session.id

        return self.respond(start_response, response)

        
    def respond(self, start_response, response):
        start_response(response.status, response.headers)
        if response.body and response.body is not None and response.body != [] and response.body != [None]:
            return response.body
        else:
            return ['']

            
    def send_error_mail(self, req, exc):
        emailer = self.emailers.Error(self.config, self.template_lookup)
        msg = emailer.error_mail(req, exc)
        if self.config.Email.delivery == 'smtp':
            if self.DEBUG == True: print 'Sending error mail.'
            emailer.send(msg)
        elif self.config.Email.deliver == 'test' or self.config.Site.status == 'test':
            print msg
            
            
    def run_server(self, port=None):
        import wsgiref.simple_server
        port = port or (self.config.Site.port and int(self.config.Site.port)) or 8000
        print "serving on port", port
        wsgiref.simple_server.make_server('', port, self).serve_forever()

        
# helper functions

def get_classes(module):
    """within the given module, get all classes"""
    classes = [module.__dict__[cls] for cls in reload(module).__dict__ if type(module.__dict__[cls])==types.TypeType]
    for _, mname, _ in pkgutil.iter_modules(module.__path__):
        modname = module.__name__+'.'+mname
        mod = reload(eval("__import__('%s')." % modname + '.'.join(modname.split('.')[1:])))
        c = [mod.__dict__[cls] for cls in mod.__dict__ if type(mod.__dict__[cls])==types.TypeType]
        classes += c
    return list(set(classes))
